﻿using System;
using System.Linq.Expressions;
using Glue.Converters;
using Glue.Internals;
using Glue.Internals.Nulls;
using Glue.Internals.Verification;

namespace Glue
{
    public class Mapping<LeftType, RightType>
    {
        internal Maybe<Func<RightType, LeftType>> LeftTypeCreator { get; private set;}
        internal Maybe<Func<LeftType, RightType>> RightTypeCreator { get; private set;}
        private readonly ConverterContainer converters = new ConverterContainer();
        private readonly CreatorContainer creators = new CreatorContainer();
        internal readonly RelationContainer<LeftType, RightType> Relations;
        internal readonly MemberContainer Ignored;
        private readonly ObjectMemberFactory objectMemberFactory;
        private readonly RelationFactory relationFactory;
        private readonly MapState mapstate;

        public Mapping()
        {
            LeftTypeCreator = Maybe<Func<RightType, LeftType>>.Nothing();
            RightTypeCreator = Maybe<Func<LeftType, RightType>>.Nothing();
            mapstate = new MapState(new NestedObjectCreator(creators), new MapState.DoNothingState());
            objectMemberFactory = new ObjectMemberFactory( mapstate);
            Relations = new RelationContainer<LeftType, RightType>(objectMemberFactory);
            Ignored = MemberContainer.CreateContainer();
            relationFactory = new RelationFactory(converters);
        }

        public Mapping(Func<RightType, LeftType> leftTypeCreator, Func<LeftType, RightType> rightTypeCreator): this()
        {
            LeftTypeCreator = new Maybe<Func<RightType, LeftType>>(leftTypeCreator);
            RightTypeCreator = new Maybe<Func<LeftType, RightType>>(rightTypeCreator);
        }

        public void Relate(Expression<Func<LeftType, object>> leftSide, Expression<Func<RightType, object>> rightSide)
        {
            TryAddRelation(objectMemberFactory.CreateObjectMember(leftSide), objectMemberFactory.CreateObjectMember(rightSide), RelationDirection.TwoWays);
        }

        public void Relate<T1, T2>(Expression<Func<LeftType, object>> leftSide, Expression<Func<RightType, object>> rightSide, Mapping<T1, T2> converter)
        {
            Relate(leftSide, rightSide, GetConverter(converter));
        }

        public void Relate(Expression<Func<LeftType, object>> leftSide, Expression<Func<RightType, object>> rightSide, IConverter converter)
        {
            TryAddRelationWithConverter(objectMemberFactory.CreateObjectMember(leftSide), objectMemberFactory.CreateObjectMember(rightSide), RelationDirection.TwoWays,converter);
        }

        public void RelateTowardsRight(Expression<Func<LeftType, object>> leftSide, Expression<Func<RightType, object>> rightSide)
        {
            TryAddRelation(objectMemberFactory.CreateObjectMember(leftSide), objectMemberFactory.CreateObjectMember(rightSide), RelationDirection.TowardsRight);
        }

        public void RelateTowardsRight<T1, T2>(Expression<Func<LeftType, object>> leftSide, Expression<Func<RightType, object>> rightSide, Mapping<T1, T2> converter)
        {
            RelateTowardsRight(leftSide, rightSide, GetConverter(converter));
        }

        public void RelateTowardsRight(Expression<Func<LeftType, object>> leftSide, Expression<Func<RightType, object>> rightSide, IConverter converter)
        {
            TryAddRelationWithConverter(objectMemberFactory.CreateObjectMember(leftSide), objectMemberFactory.CreateObjectMember(rightSide), RelationDirection.TowardsRight, converter);
        }

        public void RelateTowardsLeft(Expression<Func<LeftType, object>> leftSide, Expression<Func<RightType, object>> rightSide)
        {
            TryAddRelation(objectMemberFactory.CreateObjectMember(leftSide), objectMemberFactory.CreateObjectMember(rightSide), RelationDirection.TowardsLeft);
        }

        public void RelateTowardsLeft<T1, T2>(Expression<Func<LeftType, object>> leftSide, Expression<Func<RightType, object>> rightSide, Mapping<T1, T2> converter)
        {
            RelateTowardsLeft(leftSide, rightSide, GetConverter(converter));
        }

        public void RelateTowardsLeft(Expression<Func<LeftType, object>> leftSide, Expression<Func<RightType, object>> rightSide, IConverter converter)
        {
            TryAddRelationWithConverter(objectMemberFactory.CreateObjectMember(leftSide), objectMemberFactory.CreateObjectMember(rightSide), RelationDirection.TowardsLeft,converter);
        }


        /// <summary>
        /// Is only used by relationverifications to validate that all properties have been handled
        /// </summary>
        public void IgnoreTowards<T>(Expression<Func<T, object>> propertyToIgnore)
        {
            AssertTypeIsCorrect(typeof (T));
            var member = objectMemberFactory.CreateObjectMember(propertyToIgnore);
            AssertNoRelationExists(typeof (T), member);
            if (typeof(T) == typeof(LeftType))
                Ignored.AddToLeftSide(member);
            else
                Ignored.AddToRightSide(member);
        }

        /// <summary>
        /// Is only used by relationverifications to validate that all properties have been handled
        /// </summary>
        /// <param name="comment">Why did you ignore it? May be used in the future</param>
        public void IgnoreTowards<T>(Expression<Func<T, object>> propertyToIgnore, String comment)
        {
            IgnoreTowards(propertyToIgnore);
        }

        public IRelationsVerification GetRelationsVerification()
        {
            return new RelationsVerification<LeftType,RightType>(this, objectMemberFactory);
        }

        public IMapperVerification<LeftType,RightType> GetMapperVerification()
        {
            return new MapperVerification<LeftType, RightType>(this);
        }

        public IAutoRelateResult<LeftType, RightType> AutoRelateEqualNames()
        {
            return AutoRelateEqualNames(false);
        }

        public IAutoRelateResult<LeftType, RightType> AutoRelateEqualNames(bool acceptNullablesAsNormalTypes)
        {
            return AutoRelateEqualNames(acceptNullablesAsNormalTypes, false);
        }

        public IAutoRelateResult<LeftType, RightType> AutoRelateEqualNames(bool acceptNullablesAsNormalTypes, bool includeFields)
        {
            return AutoRelateEqualNames(acceptNullablesAsNormalTypes, includeFields, false);
        }

        public IAutoRelateResult<LeftType, RightType> AutoRelateEqualNames(bool acceptNullablesAsNormalTypes, bool includeFields,bool caseInsensitive)
        {
            var originalAcceptNullables = converters.AcceptNullablesAsNormalTypes;
            converters.AcceptNullablesAsNormalTypes = acceptNullablesAsNormalTypes;
            var autoRelater = new AutoRelater<LeftType, RightType>(relationFactory, Relations, Ignored, includeFields,caseInsensitive, objectMemberFactory);
            autoRelater.AutoRelateEqualNames();
            converters.AcceptNullablesAsNormalTypes = originalAcceptNullables;
            return autoRelater;
        }

        public void AddConverter(IConverter convert)
        {
            converters.Add(convert);
        }

        public void AddConverter<T1, T2>(Mapping<T1, T2> mapping)
        {
            converters.Add(GetConverter(mapping));
        }


        /// <summary>
        /// Strict mapping. If some objects (parents) are set to null, an exception will be thrown. Use Strict mapping
        /// when you want total control of the creation of objects on the destination object.
        /// </summary>
        public virtual RightType Map(LeftType from, RightType to)
        {
            mapstate.SetStrictState();

            return InternalMap(from, to);
        }

        private RightType InternalMap(LeftType from, RightType to)
        {
            AssertNotNullWhenMappingFrom(from);
            AssertNotNullWhenMappingTo(to);

            var mapper = new Mapper<LeftType, RightType>(Relations);
            mapper.Map(from, to);
            return to;
        }

        /// <summary>
        /// Strict mapping. If some objects (parents) are set to null, an exception will be thrown. Use Strict mapping
        /// when you want total control of the creation of objects on the destination object.
        /// </summary>
        public virtual LeftType Map(RightType from, LeftType to)
        {
            mapstate.SetStrictState();

            return InternalMap(from, to);
        }

        private LeftType InternalMap(RightType from, LeftType to)
        {
            AssertNotNullWhenMappingFrom(from);
            AssertNotNullWhenMappingTo(to);

            var mapper = new Mapper<LeftType, RightType>(Relations);
            mapper.Map(from, to);
            return to;
        }

        /// <summary>
        /// Automagical mapping: If some objects (parents) are set to null, Glue will try to create them, 
        /// by either using a provided creator or if it does not exist, by using a parameterless constructor.
        /// (Provide creators by using Mapping.AddCreator, or through the Mapping constructor), . Use Automagical mapping
        /// when you "just want it to work" and do not need total control of creation.
        /// </summary>
        public LeftType Map(RightType from)
        {
            mapstate.SetAutomagicalState();

            if (LeftTypeCreator.HasValue())
                return Map(from, LeftTypeCreator.GetValue()(from));
            if (creators.Contains(typeof(LeftType)))
                return InternalMap(from, (LeftType) creators.Get(typeof (LeftType))(null));

            throw new GlueException("No creator present. You must supply a creator for the type '"+typeof(LeftType).GetReadableName()+"' in order to use this Map-method or use Map(LeftType,RightType) instead.");
        }

        /// <summary>
        /// Automagical mapping: If some objects (parents) are set to null, Glue will try to create them, 
        /// by either using a provided creator or if it does not exist, by using a parameterless constructor.
        /// (Provide creators by using Mapping.AddCreator, or through the Mapping constructor), . Use Automagical mapping
        /// when you "just want it to work" and do not need total control of creation.
        /// </summary>
        public RightType Map(LeftType from)
        {
            mapstate.SetAutomagicalState();

            if (RightTypeCreator.HasValue())
            {
                return Map(from, RightTypeCreator.GetValue()(from));
            }
            if (creators.Contains(typeof(RightType)))
            {
                return InternalMap(from, (RightType) creators.Get(typeof (RightType))(null));
            }

            throw new GlueException("No creator present. You must supply a creator for the type '" + typeof(RightType).GetReadableName() + "' in order to use this Map-method or use Map(RightType,LeftType) instead.");

            
        }

        private void AssertNotNullWhenMappingTo<T>(T value)
        {
            if ((object)default(T) == null && (object)value == null)
            {
                throw new GlueException("Object to map to ('" + typeof(T).GetReadableName() + "') is null");
            }
        }

        private void AssertNotNullWhenMappingFrom<T>(T value)
        {
            if ((object)default(T) == null && (object)value == null)
            {
                throw new GlueException("Object to map from ('" + typeof(T).GetReadableName() + "') is null");
            }
        }

        private void AssertNoRelationExists(Type type, IObjectMember member)
        {
            if ((type == typeof(LeftType) && Relations.IsRelatedTowardsOnLeftSide(member))
                || (type == typeof(RightType) && Relations.IsRelatedTowardsOnRightSide(member)))
                throw new GlueException("The member ('" + member.Name + "') you're trying to Ignore is already related.");
        }

        private void AssertTypeIsCorrect(Type type)
        {
            if (type == typeof(LeftType)) return;
            if (type == typeof(RightType)) return;
            throw new GlueException("You have to use either '" + typeof(LeftType).Name + "' or '" + typeof(RightType).Name + "' as type. You tried to use '" + type.Name + "'");
        }

        private void TryAddRelation(
            IObjectMember leftProperty, IObjectMember rightProperty, RelationDirection direction)
        {
            var relation = relationFactory.Create(leftProperty, rightProperty, direction);
            Relations.Add(relation);
        }

        private void TryAddRelationWithConverter(
            IObjectMember leftProperty, IObjectMember rightProperty, RelationDirection direction, IConverter converter)
        {
            var relation = relationFactory.Create(leftProperty, rightProperty, direction, converter);
            Relations.Add(relation);
        }

        /// <summary>
        /// Specify a mapping between the nested type (specified as first argument) and the base type.
        /// </summary>
        /// <typeparam name="T1">-</typeparam>
        /// <param name="leftSide">The nested type</param>
        /// <param name="mapping">Mapping</param>
        public void Flatten<T1>(Expression<Func<LeftType,object>> leftSide, Mapping<T1, RightType> mapping)
        {
            var converter = GetConverter(mapping);
            TryAddRelationWithConverter(objectMemberFactory.CreateObjectMember(leftSide), 
                objectMemberFactory.CreatePropertyRepresentingBaseObject(typeof(RightType)),
                RelationDirection.TwoWays,converter);
        }

        public void Flatten<T1>(Expression<Func<RightType, object>> rightSide, Mapping<LeftType,T1> mapping)
        {
            var converter = GetConverter(mapping);
            TryAddRelationWithConverter(
                objectMemberFactory.CreatePropertyRepresentingBaseObject(typeof(LeftType)),
                objectMemberFactory.CreateObjectMember(rightSide), 
                RelationDirection.TwoWays, converter);
        }

        private IConverter GetConverter<T1, T2>(Mapping<T1, T2> mapping)
        {
            return new MappingMapping<T1, T2>(mapping);
        }

        public void AddCreator(Type createsObjectsOfThisType, Func<object, object> creator)
        {
            creators.Add(createsObjectsOfThisType, creator);
        }
    }
}